# Copyright 2023 The go-python Authors.  All rights reserved.
# Use of this source code is governed by a BSD-style
# license that can be found in the LICENSE file.

# Copied from Python-3.4.9\Lib\test\test_decorators.py

import libtest as self

def funcattrs(**kwds):
    def decorate(func):
        # FIXME func.__dict__.update(kwds)
        for k, v in kwds.items():
            func.__dict__[k] = v
        return func
    return decorate

class MiscDecorators (object):
    @staticmethod
    def author(name):
        def decorate(func):
            func.__dict__['author'] = name
            return func
        return decorate

# -----------------------------------------------

class DbcheckError (Exception):
    def __init__(self, exprstr, func, args, kwds):
        # A real version of this would set attributes here
        Exception.__init__(self, "dbcheck %r failed (func=%s args=%s kwds=%s)" %
                           (exprstr, func, args, kwds))


def dbcheck(exprstr, globals=None, locals=None):
    "Decorator to implement debugging assertions"
    def decorate(func):
        expr = compile(exprstr, "dbcheck-%s" % func.__name__, "eval")
        def check(*args, **kwds):
            if not eval(expr, globals, locals):
                raise DbcheckError(exprstr, func, args, kwds)
            return func(*args, **kwds)
        return check
    return decorate

# -----------------------------------------------

def countcalls(counts):
    "Decorator to count calls to a function"
    def decorate(func):
        func_name = func.__name__
        counts[func_name] = 0
        def call(*args, **kwds):
            counts[func_name] += 1
            return func(*args, **kwds)
        call.__name__ = func_name
        return call
    return decorate

# -----------------------------------------------

# FIXME: dict can only have string keys
# def memoize(func):
#     saved = {}
#     def call(*args):
#         try:
#             return saved[args]
#         except KeyError:
#             res = func(*args)
#             saved[args] = res
#             return res
#         except TypeError:
#             # Unhashable argument
#             return func(*args)
#     call.__name__ = func.__name__
#     return call
def memoize(func):
    saved = {}
    def call(*args):
        try:
            if isinstance(args[0], list):
                raise TypeError
            return saved[str(args)]
        except KeyError:
            res = func(*args)
            saved[str(args)] = res
            return res
        except TypeError:
            # Unhashable argument
            return func(*args)
    call.__name__ = func.__name__
    return call

# -----------------------------------------------

doc="test_single"
# FIXME staticmethod
# class C(object):
#     @staticmethod
#     def foo(): return 42
# self.assertEqual(C.foo(), 42)
# self.assertEqual(C().foo(), 42)

doc="test_staticmethod_function"
@staticmethod
def notamethod(x):
    return x
self.assertRaises(TypeError, notamethod, 1)

doc="test_dotted"
# FIXME class decorator
# decorators = MiscDecorators()
# @decorators.author('Cleese')
# def foo(): return 42
# self.assertEqual(foo(), 42)
# self.assertEqual(foo.author, 'Cleese')

doc="test_argforms"
def noteargs(*args, **kwds):
    def decorate(func):
        setattr(func, 'dbval', (args, kwds))
        return func
    return decorate

args = ( 'Now', 'is', 'the', 'time' )
kwds = dict(one=1, two=2)
@noteargs(*args, **kwds)
def f1(): return 42
self.assertEqual(f1(), 42)
self.assertEqual(f1.dbval, (args, kwds))

@noteargs('terry', 'gilliam', eric='idle', john='cleese')
def f2(): return 84
self.assertEqual(f2(), 84)
self.assertEqual(f2.dbval, (('terry', 'gilliam'),
                            dict(eric='idle', john='cleese')))

@noteargs(1, 2,)
def f3(): pass
self.assertEqual(f3.dbval, ((1, 2), {}))

doc="test_dbcheck"
# FIXME TypeError: "catching 'BaseException' that does not inherit from BaseException is not allowed"
# @dbcheck('args[1] is not None')
# def f(a, b):
#     return a + b
# self.assertEqual(f(1, 2), 3)
# self.assertRaises(DbcheckError, f, 1, None)

doc="test_memoize"
counts = {}

@memoize
@countcalls(counts)
def double(x):
    return x * 2
self.assertEqual(double.__name__, 'double')

self.assertEqual(counts, dict(double=0))

# Only the first call with a given argument bumps the call count:
#
# Only the first call with a given argument bumps the call count:
#
self.assertEqual(double(2), 4)
self.assertEqual(counts['double'], 1)
self.assertEqual(double(2), 4)
self.assertEqual(counts['double'], 1)
self.assertEqual(double(3), 6)
self.assertEqual(counts['double'], 2)

# Unhashable arguments do not get memoized:
#
self.assertEqual(double([10]), [10, 10])
self.assertEqual(counts['double'], 3)
self.assertEqual(double([10]), [10, 10])
self.assertEqual(counts['double'], 4)

doc="test_errors"
# Test syntax restrictions - these are all compile-time errors:
#
for expr in [ "1+2", "x[3]", "(1, 2)" ]:
    # Sanity check: is expr is a valid expression by itself?
    compile(expr, "testexpr", "exec")

    codestr = "@%s\ndef f(): pass" % expr
    self.assertRaises(SyntaxError, compile, codestr, "test", "exec")

# You can't put multiple decorators on a single line:
#
self.assertRaises(SyntaxError, compile,
                  "@f1 @f2\ndef f(): pass", "test", "exec")

# Test runtime errors

def unimp(func):
    raise NotImplementedError
context = dict(nullval=None, unimp=unimp)

for expr, exc in [ ("undef", NameError),
                   ("nullval", TypeError),
                   ("nullval.attr", NameError), # FIXME    ("nullval.attr", AttributeError),
                   ("unimp", NotImplementedError)]:
    codestr = "@%s\ndef f(): pass\nassert f() is None" % expr
    code = compile(codestr, "test", "exec")
    self.assertRaises(exc, eval, code, context)

doc="test_double"
class C(object):
    @funcattrs(abc=1, xyz="haha")
    @funcattrs(booh=42)
    def foo(self): return 42
self.assertEqual(C().foo(), 42)
self.assertEqual(C.foo.abc, 1)
self.assertEqual(C.foo.xyz, "haha")
self.assertEqual(C.foo.booh, 42)


doc="test_order"
# Test that decorators are applied in the proper order to the function
# they are decorating.
def callnum(num):
    """Decorator factory that returns a decorator that replaces the
    passed-in function with one that returns the value of 'num'"""
    def deco(func):
        return lambda: num
    return deco
@callnum(2)
@callnum(1)
def foo(): return 42
self.assertEqual(foo(), 2,
                 "Application order of decorators is incorrect")


doc="test_eval_order"
# Evaluating a decorated function involves four steps for each
# decorator-maker (the function that returns a decorator):
#
#    1: Evaluate the decorator-maker name
#    2: Evaluate the decorator-maker arguments (if any)
#    3: Call the decorator-maker to make a decorator
#    4: Call the decorator
#
# When there are multiple decorators, these steps should be
# performed in the above order for each decorator, but we should
# iterate through the decorators in the reverse of the order they
# appear in the source.
# FIXME class decorator
# actions = []
#
# def make_decorator(tag):
#     actions.append('makedec' + tag)
#     def decorate(func):
#         actions.append('calldec' + tag)
#         return func
#     return decorate
#
# class NameLookupTracer (object):
#     def __init__(self, index):
#         self.index = index
#
#     def __getattr__(self, fname):
#         if fname == 'make_decorator':
#             opname, res = ('evalname', make_decorator)
#         elif fname == 'arg':
#             opname, res = ('evalargs', str(self.index))
#         else:
#             assert False, "Unknown attrname %s" % fname
#         actions.append('%s%d' % (opname, self.index))
#         return res
#
# c1, c2, c3 = map(NameLookupTracer, [ 1, 2, 3 ])
#
# expected_actions = [ 'evalname1', 'evalargs1', 'makedec1',
#                      'evalname2', 'evalargs2', 'makedec2',
#                      'evalname3', 'evalargs3', 'makedec3',
#                      'calldec3', 'calldec2', 'calldec1' ]
#
# actions = []
# @c1.make_decorator(c1.arg)
# @c2.make_decorator(c2.arg)
# @c3.make_decorator(c3.arg)
# def foo(): return 42
# self.assertEqual(foo(), 42)
#
# self.assertEqual(actions, expected_actions)
#
# # Test the equivalence claim in chapter 7 of the reference manual.
# #
# actions = []
# def bar(): return 42
# bar = c1.make_decorator(c1.arg)(c2.make_decorator(c2.arg)(c3.make_decorator(c3.arg)(bar)))
# self.assertEqual(bar(), 42)
# self.assertEqual(actions, expected_actions)

doc="test_simple"
def plain(x):
    x.extra = 'Hello'
    return x
@plain
class C(object): pass
self.assertEqual(C.extra, 'Hello')

doc="test_double"
def ten(x):
    x.extra = 10
    return x
def add_five(x):
    x.extra += 5
    return x

@add_five
@ten
class C(object): pass
self.assertEqual(C.extra, 15)

doc="test_order"
def applied_first(x):
    x.extra = 'first'
    return x
def applied_second(x):
    x.extra = 'second'
    return x
@applied_second
@applied_first
class C(object): pass
self.assertEqual(C.extra, 'second')
doc="finished"
